﻿using Masuit.Tools;
using NLog;
using System.Text.RegularExpressions;

namespace Mohe.Scada.Util.S7Parser
{
    public class S7DBParser
    {
        private static ILogger logger = LogManager.GetCurrentClassLogger();
        private const string TypeDelimiter = "TYPE";
        private const string TypeEndDelimiter = "END_TYPE";
        private const string DataBlockDelimiter = "DATA_BLOCK";
        private const string DataBlockEndDelimiter = "END_DATA_BLOCK";
        private const string StructDelimiter = "STRUCT";
        private const string StructEndDelimiter = "END_STRUCT";

        private Dictionary<string, DefinitionSection> structTypes = new Dictionary<string, DefinitionSection>();

        /// <summary>
        /// 解析结构体
        /// </summary>
        /// <param name="lines"></param>
        /// <returns></returns>
        private List<DefinitionSection> ParseStruct(List<string> lines)
        {
            lines = lines.GetRange(1, lines.Count - 2);
            List<DefinitionSection> fields = new List<DefinitionSection>();
            int insideInPlaceStruct = 0;
            List<string> inPlaceStructLines = new List<string>();
            DefinitionSection currentInPlaceStruct = null;
            foreach (var line in lines)
            {
                var trimmedLine = line.Trim();

                var isStructStart = trimmedLine.Contains($": Struct") || trimmedLine.Contains("of Struct");
                var isStructEnd = trimmedLine.StartsWith(StructEndDelimiter);

                if (isStructStart)
                {
                    insideInPlaceStruct += 1;
                }

                if (insideInPlaceStruct > 0)
                {
                    inPlaceStructLines.Add(trimmedLine);
                    if (isStructEnd)
                    {
                        insideInPlaceStruct -= 1;
                        if (insideInPlaceStruct == 0)
                        {
                            currentInPlaceStruct = new DefinitionSection();
                            currentInPlaceStruct.Name = inPlaceStructLines[0].Split(':')[0].Trim();
                            currentInPlaceStruct.DataType = inPlaceStructLines[0].Split(':')[1].Trim();
                            currentInPlaceStruct.StructItems = ParseStruct(inPlaceStructLines);


                            var field = new DefinitionSection();
                            field.Name = currentInPlaceStruct.Name;
                            field.StructItems = currentInPlaceStruct.StructItems;
                            field.DataType = currentInPlaceStruct.DataType;
                            field.IsNewStruct = true;
                            if (field.StructItems != null)
                            {
                                field.StructItems.FirstOrDefault().IsNewStruct = true;
                            }

                            fields.Add(field);

                            inPlaceStructLines = new List<string>();
                        }
                    }
                }
                else if (insideInPlaceStruct == 0 && isStructEnd)
                {
                    //throw new InvalidOperationException("Trying to end a struct which was never started");
                }
                else if (!isStructStart)
                {
                    if (trimmedLine.Contains("S7_SetPoint"))
                    {
                        // a default value is specified, we ignore it
                        trimmedLine = Regex.Replace(trimmedLine, @"{ S7_SetPoint.*?}", "");
                    }

                    if (trimmedLine.Contains("{"))
                    {
                        trimmedLine = Regex.Replace(trimmedLine, @"{.*}", "");
                    }

                    if (trimmedLine.Contains("//"))
                    {
                        // ignore the comments
                        trimmedLine = Regex.Replace(trimmedLine, @"//.*", "");
                    }

                    var splits = trimmedLine.Split(':');
                    if (splits.Length >= 2)
                    {
                        var field = new DefinitionSection();
                        field.Name = splits[0].Trim();
                        field.DataType = splits[1].Trim().Replace(";", "").Replace("\"", "");
                        fields.Add(field);
                    }
                }
            }

            return fields;
        }
        private DefinitionSection ParseStructType(DefinitionSection section, string startDelimiter, string endDelimiter)
        {
            var lines = section.Raw.Split('\n').ToList();
            lines = lines.Select(l => l.Trim()).Where(l => l != "").ToList();
            int startIndex = -1;
            int endIndex = -1;
            int structStartIndex = -1;
            int structEndIndex = -1;
            for (var index = 0; index < lines.Count; index++)
            {
                var line = lines[index];
                var trimmedLine = line.Trim();
                if (trimmedLine.StartsWith(startDelimiter))
                {
                    startIndex = index;
                }
                else if (trimmedLine.StartsWith(endDelimiter))
                {
                    endIndex = index;
                }
                if (trimmedLine.StartsWith(StructDelimiter) || trimmedLine.StartsWith("VAR"))
                {
                    structStartIndex = index;
                }
                else if (trimmedLine.StartsWith(StructEndDelimiter) || trimmedLine.StartsWith("END_VAR"))
                {
                    structEndIndex = index;
                }
            }

            // Check everything is correct
            if (endIndex <= startIndex || structEndIndex <= structStartIndex)
            {
                logger.Error("An unexpected definition has been encountered");
                return null;
            }
            if (structStartIndex < 0)
            {
                structStartIndex = startIndex;
            }
            section.Name = lines[startIndex].Replace(startDelimiter, "").Replace("\"", "").Trim();
            section.StructItems = ParseStruct(lines.GetRange(structStartIndex, structEndIndex - structStartIndex + 1));

            var newStructType = CreateNewStruct(section);

            if (newStructType.Type == DefinitionType.DataBlock)
            {
                newStructType.Size = CalcOffset(newStructType) / 8 + 1;
                foreach (var structItem in newStructType.StructItems)
                {
                    if (!string.IsNullOrEmpty(structItem.DataType))
                    {
                        structItem.Name = $"\"{newStructType.Name}\".{structItem.Name}";
                        logger.Info($"\"{newStructType.Name}\".{structItem.Name}:{structItem.DataType} Pos:{structItem.Pos}  Bit:{structItem.Bit}");
                    }
                }
            }
            if (section.Type == DefinitionType.UserDefinedType)
                structTypes.Add(newStructType.Name, newStructType);
            return newStructType;
        }

        private void ExpandChildItem(List<DefinitionSection> allList, DefinitionSection source)
        {
            List<DefinitionSection> newItems = new List<DefinitionSection>();
            if (source.StructItems != null)
            {
                var test = source.StructItems;
                for (int q = 0; q < source.StructItems.Count; q++)
                {
                    var item = source.StructItems[q];
                    var matches = new Regex(@"Array\[(?<startIndex>.*)\.\.(?<endIndex>.*)\]").Match(item.DataType);
                    if (matches.Success)
                    {
                        var startIndex = int.Parse(matches.Groups["startIndex"].Value);
                        var endIndex = int.Parse(matches.Groups["endIndex"].Value);
                        var typeString = item.DataType.Replace(" of ", ":");
                        var splits = typeString.Split(':');
                        var dataTypeString = splits[1].Replace("\"", "").Trim();
                        for (int i = startIndex; i <= endIndex; i++)
                        {
                            var fieldItem = new DefinitionSection();
                            fieldItem.Name = $"{item.Name}[{i}]";
                            fieldItem.DataType = dataTypeString;
                            fieldItem.StructItems = item.StructItems.DeepClone();
                            if (structTypes.TryGetValue(dataTypeString, out DefinitionSection structType))
                            {
                                fieldItem.DataType = "Struct";
                                fieldItem.StructItems = structType.StructItems.DeepClone();
                            }
                            if (fieldItem.StructItems != null)
                            {
                                fieldItem.StructItems.FirstOrDefault().IsNewStruct = true;

                            }
                            if (i == startIndex)
                            {
                                fieldItem.IsNewStruct = true;
                            }
                            if (i == endIndex)
                            {
                                fieldItem.isEndStruct = true;
                            }
                            newItems.Add(fieldItem);
                        }

                    }
                    else if (structTypes.TryGetValue(item.DataType, out DefinitionSection structType))
                    {
                        var fieldItem = new DefinitionSection();
                        fieldItem.Name = item.Name;
                        fieldItem.DataType = "Struct";
                        fieldItem.StructItems = structType.StructItems.DeepClone();
                        newItems.Add(fieldItem);
                    }
                    else
                    {
                        if (item.DataType.StartsWith("Struct"))
                            item.IsNewStruct = true;
                        newItems.Add(item);
                    }
                }


                source.StructItems = newItems;

                if (source.StructItems.Count > 0)
                {
                    foreach (var item in source.StructItems)
                    {
                        item.Name = $"{source.Name}.{item.Name}";
                        if (!item.DataType.StartsWith("Struct"))
                            allList.Add(item);
                        ExpandChildItem(allList, item);
                    }
                }
            }
        }
        private DefinitionSection CreateNewStruct(DefinitionSection source)
        {
            List<DefinitionSection> structItems = new List<DefinitionSection>();
            List<DefinitionSection> newItems = new List<DefinitionSection>();
            foreach (var item in source.StructItems)
            {
                var matches = new Regex(@"Array\[(?<startIndex>.*)\.\.(?<endIndex>.*)\]").Match(item.DataType);
                if (matches.Success)
                {
                    var startIndex = int.Parse(matches.Groups["startIndex"].Value);
                    var endIndex = int.Parse(matches.Groups["endIndex"].Value);
                    var typeString = item.DataType.Replace(" of ", ":");
                    var splits = typeString.Split(':');
                    var dataTypeString = splits[1].Replace("\"", "").Trim();
                    for (int i = startIndex; i <= endIndex; i++)
                    {
                        var fieldItem = new DefinitionSection();
                        fieldItem.Name = $"{item.Name}[{i}]";
                        fieldItem.DataType = dataTypeString;
                        fieldItem.StructItems = item.StructItems.DeepClone();
                        if (structTypes.TryGetValue(dataTypeString, out DefinitionSection structType))
                        {
                            fieldItem.DataType = "Struct";
                            fieldItem.StructItems = structType.StructItems.DeepClone();
                        }
                        if (fieldItem.StructItems != null)
                        {
                            fieldItem.StructItems.FirstOrDefault().IsNewStruct = true;

                        }
                        if (i == startIndex)
                        {
                            fieldItem.IsNewStruct = true;
                        }
                        if (i == endIndex)
                        {
                            fieldItem.isEndStruct = true;
                        }
                        newItems.Add(fieldItem);
                    }

                }
                else
                {
                    if (item.DataType.StartsWith("Struct"))
                        item.IsNewStruct = true;
                    newItems.Add(item);
                }

            }
            foreach (var item in newItems)
            {
                ExpandChildItem(structItems, item);
                if (!item.DataType.StartsWith("Struct"))
                    structItems.Add(item);
            }
            return new DefinitionSection { Name = source.Name, Type = source.Type, StructItems = structItems };
        }



        /// <summary>
        /// 解析db文本
        /// </summary>
        /// <param name="filePath"></param>
        /// <returns></returns>
        public List<DefinitionSection> ParseFile(string filePath)
        {
            structTypes.Clear();
            var dataBlocks = new List<DefinitionSection>();
            string[] lines;
            try
            {
                lines = File.ReadAllLines(filePath);
            }
            catch
            {
                logger.Error($"Unable to read the provided file");
                return null;
            }
            List<DefinitionSection> definitionSections = new List<DefinitionSection>();
            DefinitionSection definitionSection = new DefinitionSection();
            foreach (var line in lines)
            {
                var trimmedLine = line.Trim();
                if (trimmedLine.StartsWith(TypeDelimiter))
                {
                    if (definitionSection.Type != DefinitionType.Unknown)
                    {
                        logger.Error($"Error parsing line \"{line}\"");
                    }

                    definitionSection.Type = DefinitionType.UserDefinedType;
                }

                if (trimmedLine.StartsWith(DataBlockDelimiter))
                {
                    if (definitionSection.Type != DefinitionType.Unknown)
                    {
                        logger.Error($"Error parsing line \"{line}\"");
                    }

                    definitionSection.Type = DefinitionType.DataBlock;
                }

                if (definitionSection.Type != DefinitionType.Unknown)
                {
                    if (!line.StartsWith("VERSION"))
                        definitionSection.Raw += line + "\n";
                }

                if (trimmedLine.StartsWith(TypeEndDelimiter) || trimmedLine.StartsWith(DataBlockEndDelimiter))
                {
                    if (definitionSection.Type == DefinitionType.Unknown)
                    {
                        logger.Error($"Error parsing line \"{line}\"");
                    }

                    definitionSections.Add(definitionSection);
                    definitionSection = new DefinitionSection();
                }
            }

            foreach (var defSection in definitionSections)
            {
                if (defSection.Type == DefinitionType.UserDefinedType)
                {
                    ParseStructType(defSection, TypeDelimiter, TypeEndDelimiter);
                }
                else if (defSection.Type == DefinitionType.DataBlock)
                {
                    var db = ParseStructType(defSection, DataBlockDelimiter, DataBlockEndDelimiter);
                    if (db != null)
                        dataBlocks.Add(db);
                }
            }

            return dataBlocks;
        }

        /// <summary>
        /// 计算偏移量，结构体之间2字节对齐
        /// </summary>
        /// <param name="section"></param>
        private int CalcOffset(DefinitionSection section)
        {
            int currentOffset = 0;
            int Adjust = 0;
            foreach (var item in section.StructItems)
            {

                switch (item.DataType)
                {
                    case "Bool":
                        item.Offset = GetAlignedOffset(currentOffset, 1);
                        if (item.IsNewStruct)
                        {
                            item.Offset = GetAlignedOffset(currentOffset, 16);
                            Adjust = 0;
                        }
                        item.Offset += Adjust;
                        currentOffset = item.Offset + 1;
                        break;
                    case "Byte":
                    case "Char":
                    case "USInt":
                    case "SInt":
                        item.Offset = GetAlignedOffset(currentOffset, 8);
                        if (item.IsNewStruct)
                        {
                            item.Offset = GetAlignedOffset(currentOffset, 16);
                            Adjust = 0;
                        }
                        item.Offset += Adjust;
                        currentOffset = item.Offset + 8;
                        break;
                    case "UInt":
                    case "Int":
                    case "Word":
                        item.Offset = GetAlignedOffset(currentOffset, 16);
                        currentOffset = item.Offset + 16;
                        break;
                    case "UDInt":
                    case "DInt":
                    case "Real":
                    case "DWord":
                    case "Time":
                        item.Offset = GetAlignedOffset(currentOffset, 16);
                        currentOffset = item.Offset + 32;
                        break;
                    case "DTL":
                        item.Offset = GetAlignedOffset(currentOffset, 16);
                        currentOffset = item.Offset + 12 * 8;
                        break;
                    case "String":
                    case "WString":
                        item.Offset = GetAlignedOffset(currentOffset, 16);
                        currentOffset = item.Offset + 256 * 8;
                        break;
                    default:
                        if (item.DataType.StartsWith("String"))
                        {

                            string dataType = item.DataType.Replace("String", "").Replace("[", "").Replace("]", "");
                            if (int.TryParse(dataType, out int len))
                            {
                                item.Len = len;
                                item.Offset = GetAlignedOffset(currentOffset, 16);
                                currentOffset = item.Offset + (len + 2) * 8;
                            }
                            item.DataType = "String";

                        }
                        //throw new Exception($" {item.DataType} : data type not recognition");
                        logger.Error($" {item.DataType} : data type not recognition");
                        break;
                }

                if (item.isEndStruct)
                {
                    Adjust = 16 - currentOffset % 16;
                }
                else
                {
                    Adjust = 0;
                }
            }

            return currentOffset;
        }

        private int GetAlignedOffset(int currentSize, int typeAlignment)
        {
            var remainder = currentSize % typeAlignment;
            if (currentSize % typeAlignment == 0)
            {
                // already aligned
                return currentSize;
            }

            // eg size is 29, alignement to 16
            // 29 % 16 = 13
            // result is 29 + (16 - 13) = 32
            return currentSize + typeAlignment - remainder;
        }
    }
}
